package com.avengers.common.kit;

import com.avengers.core.bean.enums.Charset;
import com.avengers.core.bean.enums.HttpMethod;
import com.avengers.core.bean.enums.Networks;
import com.avengers.core.bean.enums.SymbolEnum;
import com.avengers.core.kit.JsonKit;
import com.avengers.core.kit.StringKit;
import com.jfinal.kit.LogKit;
import com.jfinal.kit.StrKit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.*;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;

import static com.avengers.core.bean.enums.HttpEnum.*;

/**
 * @author yu.wang
 * @since 4/13/22 12:11 PM
 **/
public class HttpKit {
    private static final Logger logger = LoggerFactory.getLogger(HttpKit.class);
    public static final String KEYWORD_IGNORE_LOG_EXCEPTION_MSG = "Server returned HTTP response code: 500 for URL";

    private HttpKit() {
    }

    /**
     * https 域名校验
     */
    private static class TrustAnyHostnameVerifier implements HostnameVerifier {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }

    /**
     * https 证书管理
     */
    private static class TrustAnyTrustManager implements X509TrustManager {
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }
    }

    private final static int connectTimeout = 19000;    // 连接超时，单位毫秒
    private final static int DEFAULT_TIMEOUT = 19000;        // 读取超时，单位毫秒

    private static final SSLSocketFactory sslSocketFactory = initSSLSocketFactory();
    private static final TrustAnyHostnameVerifier trustAnyHostnameVerifier = new TrustAnyHostnameVerifier();

    private static SSLSocketFactory initSSLSocketFactory() {
        try {
            TrustManager[] tm = {new TrustAnyTrustManager()};
            SSLContext sslContext = SSLContext.getInstance(Networks.Protocol.TLS);    // ("TLS", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static HttpURLConnection getHttpConnection(String url, String method, Map<String, String> headers, int timeout) throws IOException, NoSuchAlgorithmException, NoSuchProviderException, KeyManagementException {
        URL _url = new URL(url);
        HttpURLConnection conn = (HttpURLConnection) _url.openConnection();
        if (conn instanceof HttpsURLConnection) {
            ((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory);
            ((HttpsURLConnection) conn).setHostnameVerifier(trustAnyHostnameVerifier);
        }

        conn.setRequestMethod(method);
        conn.setDoOutput(true);
        conn.setDoInput(true);

        conn.setConnectTimeout(timeout);
        conn.setReadTimeout(timeout);

        conn.setRequestProperty(CONTENT_TYPE.value, APPLICATION_FORM.value);
        conn.setRequestProperty(USER_AGENT.value, "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36");

        if (headers != null && !headers.isEmpty()) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                conn.setRequestProperty(entry.getKey(), entry.getValue());
            }
        }

        return conn;
    }

    public static String send(String method, String url, String params, Charset charset, Map<String, String> headers) throws Exception {
        logger.info("send to url_{} headers: {} request: {}", url, JsonKit.toGson(headers), params);
        HttpURLConnection urlConnection = null;
        BufferedReader reader = null;
        try {
            urlConnection = createConnection(method, url, params, headers);
            urlConnection.connect();
            reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), charset.value));
            StringBuilder responseBuilder = new StringBuilder();
            String line = reader.readLine();

            while (null != line) {
                responseBuilder.append(line);
                line = reader.readLine();
                if (null != line) {
                    responseBuilder.append("\r\n");
                }
            }
            String responseStr = responseBuilder.toString().trim();
            logger.info("send to url_{} headers: {} response: {}", url, JsonKit.toGson(headers), responseStr);
            return responseStr;
        } finally {
            try {
                if (null != reader) {
                    reader.close();
                }
                if (null != urlConnection) {
                    urlConnection.disconnect();
                }
            } catch (IOException e) {
                logger.error(e.getMessage(), e);
            }
        }
    }

    private static HttpURLConnection createConnection(String method, String url, String params, Map<String, String> headers) throws Exception {
        URL httpsUrl;
        if (HttpMethod.GET.name().equals(method)) {
            httpsUrl = new URL(buildUrlWithQueryString(url, params));
        } else {
            httpsUrl = new URL(url);
        }
        HttpURLConnection conn = (HttpURLConnection) httpsUrl.openConnection();
        conn.setDoInput(true);
        conn.setDoOutput(true);
        conn.setRequestMethod(method);
        conn.setConnectTimeout(20000);
        conn.setReadTimeout(20000);
        if (null != headers && !headers.isEmpty()) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                conn.setRequestProperty(entry.getKey(), entry.getValue());
            }
        }

        if (StringKit.notBlank(params)) {
            if (HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method) || HttpMethod.DELETE.name().equals(method)) {
                try (OutputStream outputStream = new DataOutputStream(conn.getOutputStream())) {
                    outputStream.write(params.getBytes());
                    outputStream.flush();
                }
            }
        }
        return conn;
    }

    /**
     * Send GET request
     */
    public static String get(String url, Map<String, String> queryParas, Map<String, String> headers) {
        return get(url, queryParas, headers, Charset.UTF8);
    }

    public static String get(String url, Map<String, String> queryParas, Map<String, String> headers, Integer timeout) {
        return get(url, queryParas, headers, Charset.UTF8, timeout);
    }

    public static String get(String url, Map<String, String> queryParas, Map<String, String> headers, Charset charset) {
        return get(url, queryParas, headers, charset, null);
    }

    public static String get(String url, Map<String, String> queryParas, Map<String, String> headers, Charset charset, Integer timeout) {
        HttpURLConnection conn = null;
        try {
            conn = getHttpConnection(buildUrlWithQueryString(url, queryParas, charset), HttpMethod.GET.value, headers, null == timeout ? DEFAULT_TIMEOUT : timeout);
            conn.connect();
            return readResponseString(conn, charset);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    public static String get(String url, Map<String, String> queryParas) {
        return get(url, queryParas, null, Charset.UTF8);
    }

    public static String get(String url, Map<String, String> queryParas, Integer timeout) {
        return get(url, queryParas, null, Charset.UTF8, timeout);
    }

    public static String get(String url, Map<String, String> queryParas, Charset charset) {
        return get(url, queryParas, null, charset);
    }

    public static String get(String url, Map<String, String> queryParas, Charset charset, Integer timeout) {
        return get(url, queryParas, null, charset, timeout);
    }

    public static String get(String url) {
        return get(url, null, null, Charset.UTF8);
    }

    public static String get(String url, Integer timeout) {
        return get(url, null, null, Charset.UTF8, timeout);
    }

    public static String get(String url, Charset charset) {
        return get(url, null, null, charset);
    }

    public static String get(String url, Charset charset, Integer timeout) {
        return get(url, null, null, charset, timeout);
    }

    /**
     * Send POST request
     */
    public static String post(String url, Map<String, String> queryParas, String data, Map<String, String> headers) {
        return post(url, queryParas, data, headers, Charset.UTF8);
    }

    public static String post(String url, Map<String, String> queryParas, String data, Map<String, String> headers, Integer timeout) {
        return post(url, queryParas, data, headers, Charset.UTF8, timeout);
    }

    public static String post(String url, Map<String, String> queryParas, String data, Map<String, String> headers, Charset charset) {
        return post(url, queryParas, data, headers, charset, null);
    }

    public static String post(String url, Map<String, String> queryParas, String data, Map<String, String> headers, Charset charset, Integer timeout) {
        HttpURLConnection conn = null;
        try {
            conn = getHttpConnection(buildUrlWithQueryString(url, queryParas, charset), HttpMethod.POST.value, headers, null == timeout ? DEFAULT_TIMEOUT : timeout);
            conn.connect();

            if (data != null) {
                OutputStream out = conn.getOutputStream();
                out.write(data.getBytes(charset.value));
                out.flush();
                out.close();
            }

            return readResponseString(conn, charset);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    public static String post(String url, Map<String, String> queryParas, String data) {
        return post(url, queryParas, data, null, Charset.UTF8);
    }

    public static String post(String url, Map<String, String> queryParas, String data, Integer timeout) {
        return post(url, queryParas, data, null, Charset.UTF8, timeout);
    }

    public static String post(String url, Map<String, String> queryParas, String data, Charset charset) {
        return post(url, queryParas, data, null, charset);
    }

    public static String post(String url, Map<String, String> queryParas, String data, Charset charset, Integer timeout) {
        return post(url, queryParas, data, null, charset, timeout);
    }

    public static String post(String url, String data, Map<String, String> headers) {
        return post(url, null, data, headers, Charset.UTF8);
    }

    public static String post(String url, String data, Map<String, String> headers, Integer timeout) {
        return post(url, null, data, headers, Charset.UTF8, timeout);
    }

    public static String post(String url, String data, Map<String, String> headers, Charset charset) {
        return post(url, null, data, headers, charset);
    }

    public static String post(String url, String data, Map<String, String> headers, Charset charset, Integer timeout) {
        return post(url, null, data, headers, charset, timeout);
    }

    public static String post(String url, String data) {
        return post(url, null, data, null, Charset.UTF8);
    }

    public static String post(String url, String data, Integer timeout) {
        return post(url, null, data, null, Charset.UTF8, timeout);
    }

    public static String post(String url, String data, Charset charset) {
        return post(url, null, data, null, charset);
    }

    public static String post(String url, String data, Charset charset, Integer timeout) {
        return post(url, null, data, null, charset, timeout);
    }

    private static String readResponseString(HttpURLConnection conn, Charset charset) {
        BufferedReader reader = null;
        try {
            StringBuilder ret;
            reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), charset.value));
            String line = reader.readLine();
            if (line != null) {
                ret = new StringBuilder();
                ret.append(line);
            } else {
                return "";
            }

            while ((line = reader.readLine()) != null) {
                ret.append('\n').append(line);
            }
            return ret.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    LogKit.error(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * Build queryString of the url
     */
    private static String buildUrlWithQueryString(String url, String postStr) {
        if (postStr == null || postStr.trim().isEmpty()) {
            return url;
        }

        StringBuilder sb = new StringBuilder(url);
        if (url.contains(SymbolEnum.question.string)) {
            sb.append(SymbolEnum.and).append(postStr);
        } else {
            sb.append(SymbolEnum.question.string).append(postStr);
        }

        return sb.toString();
    }

    /**
     * Build queryString of the url
     */
    private static String buildUrlWithQueryString(String url, Map<String, String> queryParas, Charset charset) {
        if (queryParas == null || queryParas.isEmpty()) {
            return url;
        }

        StringBuilder sb = new StringBuilder(url);
        boolean isFirst;
        if (url.indexOf('?') == -1) {
            isFirst = true;
            sb.append('?');
        } else {
            isFirst = false;
        }

        for (Map.Entry<String, String> entry : queryParas.entrySet()) {
            if (isFirst) {
                isFirst = false;
            } else {
                sb.append('&');
            }

            String key = entry.getKey();
            String value = entry.getValue();
            if (StrKit.notBlank(value)) {
                try {
                    value = URLEncoder.encode(value, charset.value);
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeException(e);
                }
            }
            sb.append(key).append('=').append(value);
        }
        return sb.toString();
    }

    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder ret;
            br = request.getReader();

            String line = br.readLine();
            if (line != null) {
                ret = new StringBuilder();
                ret.append(line);
            } else {
                return "";
            }

            while ((line = br.readLine()) != null) {
                ret.append('\n').append(line);
            }

            return ret.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
		/* 去掉 close() 否则后续 ActionReporter 中的 getPara() 在部分 tomcat 中会报 IOException : Stream closed
		finally {
			if (br != null) {
				try {br.close();} catch (IOException e) {LogKit.error(e.getMessage(), e);}
			}
		}*/
    }

    @Deprecated
    public static String readIncommingRequestData(HttpServletRequest request) {
        return readData(request);
    }

    /**
     * 检测是否为 https 请求
     * <p>
     * nginx 代理实现 https 的场景，需要对 nginx 进行如下配置：
     * proxy_set_header X-Forwarded-Proto https;
     * 或者配置:
     * proxy_set_header X-Forwarded-Proto $scheme;
     */
    public static boolean isHttps(HttpServletRequest request) {
        return "https".equalsIgnoreCase(request.getHeader("X-Forwarded-Proto"))
                ||
                "https".equalsIgnoreCase(request.getScheme());
    }
}
